﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core.Emitters
{
	/// <summary>Emits a class holding read-only properties.</summary>
	public class ImmutablePropertyHolderEmitter : TypeEmitterBase<ImmutablePropertyHolderOptions>
	{
		/// <summary>Constructor, taking the options in parameter.</summary>
		/// <param name="options"></param>
		protected ImmutablePropertyHolderEmitter(ImmutablePropertyHolderOptions options)
			: base(options)
		{
		}

		#region Factory Methods
		/// <summary>Creates a factory of instance of immutable property holder.</summary>
		/// <typeparam name="INTER">Interface type exposing properties.</typeparam>
		/// <returns>A factory of instance of immutable property holder.</returns>
		public static object GetFactory<INTER>()
			where INTER : class
		{
			return GetFactory<INTER, object>();
		}

		/// <summary>Creates a factory of instance of immutable property holder with a given base class.</summary>
		/// <typeparam name="INTER">Interface type exposing properties.</typeparam>
		/// <typeparam name="BASE">Type of the base class.</typeparam>
		/// <returns>A factory of instance of immutable property holder.</returns>
		public static object GetFactory<INTER, BASE>()
			where INTER : class
			where BASE : class
		{
			ImmutablePropertyHolderOptions options = new ImmutablePropertyHolderOptions
			{
				ExposedInterfaceType = typeof(INTER),
				BaseType = typeof(BASE)
			};
			object untypedFactory = GetFactory(options);

			return untypedFactory;
		}

		/// <summary>
		/// Creates a factory of instance of immutable property holder with a given base class taking a parameter in construction.
		/// </summary>
		/// <typeparam name="INTER">Interface type exposing properties.</typeparam>
		/// <typeparam name="BASE">Type of the base class.</typeparam>
		/// <typeparam name="BASE_CTOR_PARAM">Type of the base class constructor parameter.</typeparam>
		/// <returns>A factory of instance of immutable property holder.</returns>
		public static object GetFactory<INTER, BASE, BASE_CTOR_PARAM>()
			where INTER : class
			where BASE : class
		{
			ImmutablePropertyHolderOptions options = new ImmutablePropertyHolderOptions
			{
				ExposedInterfaceType = typeof(INTER),
				BaseType = typeof(BASE),
				BaseConstructorParameterTypeList = new Type[] { typeof(BASE_CTOR_PARAM) }
			};
			object untypedFactory = GetFactory(options);

			return untypedFactory;
		}

		/// <summary>Creates a factory of instance of immutable property holder given options.</summary>
		/// <param name="options"></param>
		/// <returns>A factory of instance of immutable property holder.</returns>
		public static object GetFactory(ImmutablePropertyHolderOptions options)
		{
			if (options == null)
			{
				throw new ArgumentNullException("options");
			}

			ImmutablePropertyHolderEmitter emitter = new ImmutablePropertyHolderEmitter(options);
			object factory = options.Cache.GetFactory(emitter);

			return factory;
		}
		#endregion

		/// <summary>Emits a factory corresponding to the state of this emitter.</summary>
		/// <param name="componentTypeName"></param>
		/// <param name="factoryTypeName"></param>
		/// <param name="moduleBuilder"></param>
		/// <returns></returns>
		public override object EmitFactory(
			string componentTypeName, string factoryTypeName, ModuleBuilderExtender moduleBuilder)
		{
			IList<Type> allInterfaces = TypeHelper.GetAllInterfaces(Options.ExposedInterfaceType, true);
			TypeBuilderExtender typeBuilder = moduleBuilder.CreateType(
				componentTypeName,
				Options.BaseType,
				Options.ExposedInterfaceType);
			Type[] baseClassInterfaces = Options.IsUsingBaseClassInterfaces
				? TypeHelper.GetAllInterfaces(Options.BaseType, true)
				: new Type[0];

			DefineCustomConstructor(typeBuilder, allInterfaces);
			ImplementInterface(
				typeBuilder,
				Options.ExposedInterfaceType,
				EmitGetMethod,
				null,
				null,
				null,
				null,
				baseClassInterfaces);

			Type componentType = typeBuilder.TypeBuilder.CreateType();
			object factory =
				CreateFactoryInstance(factoryTypeName, componentType, Options.ExposedInterfaceType, moduleBuilder);

			return factory;
		}

		private void DefineCustomConstructor(TypeBuilderExtender typeBuilder, IEnumerable<Type> interfaceTypeList)
		{
			const string PARAM_PREFIX = "param_";

			List<MethodParameter> parameterList = new List<MethodParameter>();
			IList<PropertyInfo> propertyList = new List<PropertyInfo>();

			//	Create method parameters
			foreach (Type interfaceType in interfaceTypeList)
			{
				foreach (PropertyInfo property in interfaceType.GetProperties())
				{
					MethodParameter param =
						new MethodParameter(string.Concat(PARAM_PREFIX, property.Name), property.PropertyType);
					
					parameterList.Add(param);
					propertyList.Add(property);
				}
			}
			ILGeneratorExtender constructorGenerator = CreateInstanceConstructor(
				typeBuilder,
				Options.BaseType,
				Options.BaseConstructorParameterTypeList,
				parameterList.ToArray());

			for (int i = 0; i != parameterList.Count; ++i)
			{
				MethodParameter parameter = parameterList[i];
				string propertyName = parameter.Name.Substring(PARAM_PREFIX.Length);
				//	Create Fields
				FieldBuilder field =
					typeBuilder.DefinePrivateField(GetFieldName(propertyName), parameter.Type, true, false);

				ValidateParameter(constructorGenerator, i + 1, parameterList[i], propertyList[i]);
				//  Store parameter in instance field
				constructorGenerator.StackMethodParameter(0);
				constructorGenerator.StackMethodParameter(i + 1);
				constructorGenerator.StoreStackInField(field);
			}
			constructorGenerator.ReturnStack();
		}

		private void ValidateParameter(
			ILGeneratorExtender constructorGenerator, int paramIndex, MethodParameter methodParameter, PropertyInfo property)
		{
			//	Validate method parameters (for reference types only, agains't null and empty strings)
			if (!methodParameter.Type.IsValueType)
			{
				object[] customAttributes = property.GetCustomAttributes(typeof(ReferenceTypeValidationAttribute), true);
				ReferenceTypeValidationLevel validationLevel = (customAttributes == null || customAttributes.Length != 1)
					? Options.ReferenceTypeValidationLevel
					: ((ReferenceTypeValidationAttribute)customAttributes[0]).ValidationLevel;
				//	Apply string empty iif the property is of type string
				ReferenceTypeValidationLevel effectiveValidationLevel =
					(validationLevel == ReferenceTypeValidationLevel.NoNullAndEmptyString)
					&& (property.PropertyType != typeof(string))
					? ReferenceTypeValidationLevel.NoNull
					: validationLevel;

				switch (effectiveValidationLevel)
				{
					case ReferenceTypeValidationLevel.NoNull:
						constructorGenerator.ValidateNotNullParameter(paramIndex, methodParameter.Name);
						break;
					case ReferenceTypeValidationLevel.NoNullAndEmptyString:
						constructorGenerator.ValidateNotNullOrEmptyParameter(paramIndex, methodParameter.Name);
						break;
					default:	//	No validation
						break;
				}
			}
		}

		private string GetFieldName(string propertyName)
		{
			return string.Concat("_", propertyName);
		}

		private void EmitGetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
		{
			ilGenerator.StackField(GetFieldName(property.Name));
			ilGenerator.ReturnStack();
		}
	}
}